package com.github.zj.dreamly.java8.snippets;

import java.io.*;
import java.lang.management.ManagementFactory;
import java.lang.management.RuntimeMXBean;
import java.lang.reflect.Array;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.AbstractMap.SimpleEntry;
import java.util.*;
import java.util.concurrent.ThreadLocalRandom;
import java.util.function.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;

public abstract class Snippets {

	/**
	 * Calculates the greatest common denominator (gcd) of an array of numbers
	 *
	 * @param numbers Array of numbers
	 * @return gcd of array of numbers
	 */
	public static OptionalInt gcd(int[] numbers) {
		return Arrays.stream(numbers)
			.reduce((a, b) -> gcd(a, b));
	}

	/**
	 * Calculates the lowest common multiple (lcm) of an array of numbers.
	 *
	 * @param numbers Array of numbers
	 * @return lcm of array of numbers
	 */
	public static OptionalInt lcm(int[] numbers) {
		IntBinaryOperator lcm = (x, y) -> (x * y) / gcd(x, y);
		return Arrays.stream(numbers)
			.reduce((a, b) -> lcm.applyAsInt(a, b));
	}

	private static int gcd(int a, int b) {
		if (b == 0) {
			return a;
		}
		return gcd(b, a % b);
	}

	/**
	 * Returns the maximum value in an array.
	 *
	 * @param numbers Array of numbers
	 * @return maximum value in an array
	 */
	public static OptionalInt arrayMax(int[] numbers) {
		return Arrays.stream(numbers).max();
	}

	/**
	 * Returns the minimum value in an array.
	 *
	 * @param numbers Array of numbers
	 * @return minimum value in an array
	 */
	public static OptionalInt arrayMin(int[] numbers) {
		return Arrays.stream(numbers).min();
	}

	/**
	 * Chunks an array into smaller arrays of a specified size.
	 *
	 * @param numbers Input array of numbers
	 * @param size    The chunk size
	 * @return Smaller chunks
	 */
	public static int[][] chunk(int[] numbers, int size) {
		return IntStream.iterate(0, i -> i + size)
			.limit((long) Math.ceil((double) numbers.length / size))
			.mapToObj(cur -> Arrays.copyOfRange(numbers, cur, cur + size > numbers.length ? numbers.length : cur + size))
			.toArray(int[][]::new);
	}

	/**
	 * Counts the occurrences of a value in an array.
	 *
	 * @param numbers Array of numbers
	 * @param value   the value for which we have to count occurrences
	 * @return count of total number of occurrences of the value
	 */
	public static long countOccurrences(int[] numbers, int value) {
		return Arrays.stream(numbers)
			.filter(number -> number == value)
			.count();
	}

	/**
	 * Deep flattens an array.
	 *
	 * @param input A nested array containing integers
	 * @return flattened array
	 */
	public static int[] deepFlatten(Object[] input) {
		return Arrays.stream(input)
			.flatMapToInt(o -> {
				if (o instanceof Object[]) {
					return Arrays.stream(deepFlatten((Object[]) o));
				}
				return IntStream.of((Integer) o);
			}).toArray();
	}

	/**
	 * Returns the difference between two arrays.
	 *
	 * @param first  the first array
	 * @param second the second array
	 * @return Elements in first that are not in second
	 */
	public static int[] difference(int[] first, int[] second) {
		Set<Integer> set = Arrays.stream(second).boxed().collect(Collectors.toSet());
		return Arrays.stream(first)
			.filter(v -> !set.contains(v))
			.toArray();
	}

	/**
	 * Filters out all values from an array for which the comparator function does not return true.
	 *
	 * @param first      the first array
	 * @param second     the second array
	 * @param comparator the comparator function
	 * @return the resulting array
	 */
	public static int[] differenceWith(int[] first, int[] second, IntBinaryOperator comparator) {
		return Arrays.stream(first)
			.filter(a ->
				Arrays.stream(second)
					.noneMatch(b -> comparator.applyAsInt(a, b) == 0)
			).toArray();
	}

	/**
	 * Returns all the distinct values of an array.
	 *
	 * @param elements ints
	 * @return distinct values
	 */
	public static int[] distinctValuesOfArray(int[] elements) {
		return Arrays.stream(elements).distinct().toArray();
	}

	/**
	 * Removes elements in an array until the passed function returns true. Returns the remaining elements in the array.
	 *
	 * @param elements
	 * @param condition
	 * @return
	 */
	public static int[] dropElements(int[] elements, IntPredicate condition) {
		while (elements.length > 0 && !condition.test(elements[0])) {
			elements = Arrays.copyOfRange(elements, 1, elements.length);
		}
		return elements;
	}

	/**
	 * Returns a new array with n elements removed from the right
	 *
	 * @param elements
	 * @param n        number of elements to remove
	 * @return array after removing n elements
	 */
	public static int[] dropRight(int[] elements, int n) {
		if (n < 0) {
			throw new IllegalArgumentException("n is less than 0");
		}
		return n < elements.length
			? Arrays.copyOfRange(elements, 0, elements.length - n)
			: new int[0];
	}

	/**
	 * Returns every nth element in an array.
	 *
	 * @param elements
	 * @param nth
	 * @return
	 */
	public static int[] everyNth(int[] elements, int nth) {
		return IntStream.range(0, elements.length)
			.filter(i -> i % nth == nth - 1)
			.map(i -> elements[i])
			.toArray();
	}

	/**
	 * Filters out the non-unique values in an array.
	 * <p>
	 * Use Array.stream().filter() for an array containing only the unique values.
	 *
	 * @param elements input array
	 * @return unique values in the array
	 */
	public static int[] filterNonUnique(int[] elements) {
		return Arrays.stream(elements)
			.filter(el -> indexOf(elements, el) == lastIndexOf(elements, el))
			.toArray();
	}

	/**
	 * Find index of element in the array. Return -1 in case element does not exist.
	 * <p>
	 * Uses IntStream.range().filter() to find index of the element in the array.
	 *
	 * @param elements input array
	 * @param el       element to find
	 * @return index of the element
	 */
	public static int indexOf(int[] elements, int el) {
		return IntStream.range(0, elements.length)
			.filter(idx -> elements[idx] == el)
			.findFirst()
			.orElse(-1);
	}

	/**
	 * Find last index of element in the array. Return -1 in case element does not exist.
	 * <p>
	 * Uses IntStream.iterate().limit().filter() to find index of the element in the array.
	 *
	 * @param elements input array
	 * @param el       element to find
	 * @return index of the element
	 */
	public static int lastIndexOf(int[] elements, int el) {
		return IntStream.iterate(elements.length - 1, i -> i - 1)
			.limit(elements.length)
			.filter(idx -> elements[idx] == el)
			.findFirst()
			.orElse(-1);
	}

	/**
	 * Flattens an array.
	 *
	 * @param elements input array
	 * @return flattened array
	 */
	public static int[] flatten(Object[] elements) {
		return Arrays.stream(elements)
			.flatMapToInt(el -> el instanceof int[]
				? Arrays.stream((int[]) el)
				: IntStream.of((int) el)
			).toArray();
	}

	/**
	 * Flattens an array up to the specified depth.
	 *
	 * @param elements input array
	 * @param depth    depth to which to flatten array
	 * @return flattened array
	 */
	public static Object[] flattenDepth(Object[] elements, int depth) {
		if (depth == 0) {
			return elements;
		}
		return Arrays.stream(elements)
			.flatMap(el -> el instanceof Object[]
				? Arrays.stream(flattenDepth((Object[]) el, depth - 1))
				: Arrays.stream(new Object[]{el})
			).toArray();

	}

	/**
	 * Groups the elements of an array based on the given function.
	 *
	 * @param elements input array
	 * @param func     function
	 * @param <T>      type parameter
	 * @return grouped elements in a Map
	 */
	public static <T, R> Map<R, List<T>> groupBy(T[] elements, Function<T, R> func) {
		return Arrays.stream(elements).collect(Collectors.groupingBy(func));
	}

	/**
	 * Returns all the elements of an array except the last one.
	 * Use Arrays.copyOfRange() to return all except the last one
	 *
	 * @param elements
	 * @param <T>
	 * @return
	 */
	public static <T> T[] initial(T[] elements) {
		return Arrays.copyOfRange(elements, 0, elements.length - 1);
	}

	/**
	 * Initializes an array containing the numbers in the specified range where start and end are inclusive.
	 *
	 * @param end
	 * @param start
	 * @return
	 */
	public static int[] initializeArrayWithRange(int end, int start) {
		return IntStream.rangeClosed(start, end).toArray();
	}

	public static int[] initializeArrayWithValues(int n, int value) {
		return IntStream.generate(() -> value).limit(n).toArray();
	}

	public static int[] intersection(int[] first, int[] second) {
		Set<Integer> set = Arrays.stream(second).boxed().collect(Collectors.toSet());
		return Arrays.stream(first)
			.filter(set::contains)
			.toArray();
	}

	public static <T extends Comparable<? super T>> int isSorted(T[] arr) {
		final int direction = arr[0].compareTo(arr[1]) < 0 ? 1 : -1;
		for (int i = 0; i < arr.length; i++) {
			T val = arr[i];
			if (i == arr.length - 1) {
				return direction;
			} else if ((val.compareTo(arr[i + 1]) * direction > 0)) {
				return 0;
			}
		}
		return direction;
	}

	public static <T> String join(T[] arr, String separator, String end) {
		return IntStream.range(0, arr.length)
			.mapToObj(i -> new SimpleEntry<>(i, arr[i]))
			.reduce("", (acc, val) -> val.getKey() == arr.length - 2
				? acc + val.getValue() + end
				: val.getKey() == arr.length - 1 ? acc + val.getValue() : acc + val.getValue() + separator, (fst, snd) -> fst);
	}

	public static <T> String join(T[] arr, String separator) {
		return join(arr, separator, separator);
	}

	public static <T> String join(T[] arr) {
		return join(arr, ",");
	}

	public static <T> T nthElement(T[] arr, int n) {
		if (n > 0) {
			return Arrays.copyOfRange(arr, n, arr.length)[0];
		}
		return Arrays.copyOfRange(arr, arr.length + n, arr.length)[0];
	}

	public static <T, R> Map<T, R> pick(Map<T, R> obj, T[] arr) {
		return Arrays.stream(arr)
			.filter(obj::containsKey)
			.collect(Collectors.toMap(k -> k, obj::get));
	}

	public static Map<String, Object>[] reducedFilter(Map<String, Object>[] data, String[] keys, Predicate<Map<String, Object>> fn) {
		return Arrays.stream(data)
			.filter(fn)
			.map(el -> Arrays.stream(keys).filter(el::containsKey)
				.collect(Collectors.toMap(Function.identity(), el::get)))
			.toArray((IntFunction<Map<String, Object>[]>) Map[]::new);
	}

	public static <T> T sample(T[] arr) {
		return arr[(int) Math.floor(Math.random() * arr.length)];
	}

	public static <T> T[] sampleSize(T[] input, int n) {
		T[] arr = Arrays.copyOf(input, input.length);
		int length = arr.length;
		int m = length;
		while (m > 0) {
			int i = (int) Math.floor(Math.random() * m--);
			T tmp = arr[i];
			arr[i] = arr[m];
			arr[m] = tmp;
		}
		return Arrays.copyOfRange(arr, 0, n > length ? length : n);
	}

	public static <T> T[] shuffle(T[] input) {
		T[] arr = Arrays.copyOf(input, input.length);
		int length = arr.length;
		int m = length;
		while (m > 0) {
			int i = (int) Math.floor(Math.random() * m--);
			T tmp = arr[i];
			arr[i] = arr[m];
			arr[m] = tmp;
		}
		return arr;
	}

	public static <T> T[] similarity(T[] first, T[] second) {
		return Arrays.stream(first)
			.filter(a -> Arrays.stream(second).anyMatch(b -> Objects.equals(a, b)))
			// Make a new array of first's runtime type, but empty content:
			.toArray(i -> (T[]) Arrays.copyOf(new Object[0], i, first.getClass()));
	}

	public static <T> T[] emptyArray(Class<T> clz) {
		return (T[]) Array.newInstance(clz, 0);
	}

	public static <T extends Comparable<? super T>> int sortedIndex(T[] arr, T el) {
		boolean isDescending = arr[0].compareTo(arr[arr.length - 1]) > 0;
		return IntStream.range(0, arr.length)
			.filter(i -> isDescending ? el.compareTo(arr[i]) >= 0 : el.compareTo(arr[i]) <= 0)
			.findFirst()
			.orElse(arr.length);
	}

	public static <T> T[] symmetricDifference(T[] first, T[] second) {
		Set<T> sA = new HashSet<>(Arrays.asList(first));
		Set<T> sB = new HashSet<>(Arrays.asList(second));

		return Stream.concat(
			Arrays.stream(first).filter(a -> !sB.contains(a)),
			Arrays.stream(second).filter(b -> !sA.contains(b))
		).toArray(i -> (T[]) Arrays.copyOf(new Object[0], i, first.getClass()));
	}

	public static <T> T[] tail(T[] arr) {
		return arr.length > 1
			? Arrays.copyOfRange(arr, 1, arr.length)
			: arr;
	}

	public static <T> T[] take(T[] arr, int n) {
		return Arrays.copyOfRange(arr, 0, n);
	}

	public static <T> T[] takeRight(T[] arr, int n) {
		return Arrays.copyOfRange(arr, arr.length - n, arr.length);
	}

	public static <T> T[] union(T[] first, T[] second) {
		Set<T> set = new HashSet<>(Arrays.asList(first));
		set.addAll(Arrays.asList(second));
		return set.toArray((T[]) Arrays.copyOf(new Object[0], 0, first.getClass()));
	}

	public static <T> T[] without(T[] arr, T... elements) {
		List<T> excludeElements = Arrays.asList(elements);
		return Arrays.stream(arr)
			.filter(el -> !excludeElements.contains(el))
			.toArray(i -> (T[]) Arrays.copyOf(new Object[0], i, arr.getClass()));
	}

	public static List<Object[]> zip(Object[]... arrays) {
		OptionalInt max = Arrays.stream(arrays).mapToInt(arr -> arr.length).max();
		return IntStream.range(0, max.getAsInt())
			.mapToObj(i -> Arrays.stream(arrays)
				.map(arr -> i < arr.length ? arr[i] : null)
				.toArray())
			.collect(Collectors.toList());
	}

	public static Map<String, Object> zipObject(String[] props, Object[] values) {
		return IntStream.range(0, props.length)
			.mapToObj(i -> new SimpleEntry<>(props[i], i < values.length ? values[i] : null))
			.collect(
				HashMap::new, (m, v) -> m.put(v.getKey(), v.getValue()), HashMap::putAll);
	}

	public static double average(int[] arr) {
		return IntStream.of(arr)
			.average()
			.orElseThrow(() -> new IllegalArgumentException("Array is empty"));
	}

	public static List<String> anagrams(String input) {
		if (input.length() <= 2) {
			return input.length() == 2
				? Arrays.asList(input, input.substring(1) + input.substring(0, 1))
				: Collections.singletonList(input);
		}
		return IntStream.range(0, input.length())
			.mapToObj(i -> new SimpleEntry<>(i, input.substring(i, i + 1)))
			.flatMap(entry ->
				anagrams(input.substring(0, entry.getKey()) + input.substring(entry.getKey() + 1))
					.stream()
					.map(s -> entry.getValue() + s))
			.collect(Collectors.toList());
	}

	public static int byteSize(String input) {
		// Read the link below to learn more
		// https://stackoverflow.com/questions/16270994/difference-between-string-length-and-string-getbytes-length
		return input.getBytes().length;
	}

	public static String capitalize(String input, boolean lowerRest) {
		return input.substring(0, 1).toUpperCase() +
			(lowerRest
				? input.substring(1, input.length()).toLowerCase()
				: input.substring(1, input.length()));
	}

	public static String capitalizeEveryWord(final String input) {
		return Pattern.compile("\\b(?=\\w)").splitAsStream(input)
			.map(w -> capitalize(w, false))
			.collect(Collectors.joining());
	}

	public static int countVowels(String input) {
		return input.replaceAll("[^aeiouAEIOU]", "").length();
	}

	public static String escapeRegExp(String input) {
		return Pattern.quote(input);
	}

	public static String fromCamelCase(String input, String separator) {
		return input
			.replaceAll("([a-z\\d])([A-Z])", "$1" + separator + "$2")
			.toLowerCase();
	}

	public static boolean isAbsoluteUrl(String url) {
		return Pattern.compile("^[a-z][a-z0-9+.-]*:").matcher(url).find();
	}

	public static boolean isLowerCase(String input) {
		return Objects.equals(input, input.toLowerCase());
	}

	public static boolean isUpperCase(String input) {
		return Objects.equals(input, input.toUpperCase());
	}

	public static String mask(String input, int num, String mask) {
		int length = input.length();
		return num > 0
			?
			input.substring(0, length - num).replaceAll(".", mask)
				+ input.substring(length - num)
			:
			input.substring(0, Math.negateExact(num))
				+ input.substring(Math.negateExact(num), length).replaceAll(".", mask);
	}

	public static boolean isPalindrome(String input) {
		String s = input.toLowerCase().replaceAll("[\\W_]", "");
		return Objects.equals(
			s,
			new StringBuilder(s).reverse().toString()
		);
	}

	public static String reverseString(String input) {
		return new StringBuilder(input).reverse().toString();
	}

	public static String sortCharactersInString(String input) {
		return Arrays.stream(input.split("")).sorted().collect(Collectors.joining());
	}

	public static String[] splitLines(String input) {
		return input.split("\\r?\\n");
	}

	public static String toCamelCase(String input) {
		Matcher matcher = Pattern.compile("[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+").matcher(input);
		List<String> matchedParts = new ArrayList<>();
		while (matcher.find()) {
			matchedParts.add(matcher.group(0));
		}
		String s = matchedParts.stream()
			.map(x -> x.substring(0, 1).toUpperCase() + x.substring(1).toLowerCase())
			.collect(Collectors.joining());
		return s.substring(0, 1).toLowerCase() + s.substring(1);
	}

	public static String toKebabCase(String input) {
		Matcher matcher = Pattern.compile("[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+").matcher(input);
		List<String> matchedParts = new ArrayList<>();
		while (matcher.find()) {
			matchedParts.add(matcher.group(0));
		}
		return matchedParts.stream()
			.map(String::toLowerCase)
			.collect(Collectors.joining("-"));
	}

	public static List<String> match(String input, String regex) {
		Matcher matcher = Pattern.compile(regex).matcher(input);
		List<String> matchedParts = new ArrayList<>();
		while (matcher.find()) {
			matchedParts.add(matcher.group(0));
		}
		return matchedParts;
	}

	public static String toSnakeCase(String input) {
		Matcher matcher = Pattern.compile("[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+").matcher(input);
		List<String> matchedParts = new ArrayList<>();
		while (matcher.find()) {
			matchedParts.add(matcher.group(0));
		}
		return matchedParts.stream()
			.map(String::toLowerCase)
			.collect(Collectors.joining("_"));
	}

	public static String truncateString(String input, int num) {
		return input.length() > num
			? input.substring(0, num > 3 ? num - 3 : num) + "..."
			: input;
	}

	public static String[] words(String input) {
		return Arrays.stream(input.split("[^a-zA-Z-]+"))
			.filter(s -> !s.isEmpty())
			.toArray(String[]::new);
	}

	//Read the link below for more information
	// https://stackoverflow.com/questions/309424/read-convert-an-inputstream-to-a-string
	public static String convertInputStreamToString(final InputStream in) throws IOException {
		ByteArrayOutputStream result = new ByteArrayOutputStream();
		byte[] buffer = new byte[1024];
		int length;
		while ((length = in.read(buffer)) != -1) {
			result.write(buffer, 0, length);
		}
		return result.toString(StandardCharsets.UTF_8.name());
	}

	public static int[] randomInts(int total, int start, int end) {
		return ThreadLocalRandom.current().ints(total, start, end).toArray();
	}

	public String readFileAsString(Path path) throws IOException {
		return new String(Files.readAllBytes(path));
	}

	public static String stackTraceAsString(final Throwable throwable) {
		final StringWriter sw = new StringWriter();
		throwable.printStackTrace(new PrintWriter(sw));
		return sw.toString();
	}

	public static <T> T[] concat(T[] first, T[] second) {
		return Stream.concat(
			Stream.of(first),
			Stream.of(second)
		).toArray(i -> (T[]) Arrays.copyOf(new Object[0], i, first.getClass()));
	}

	public static String getCurrentWorkingDirectoryPath() {
		return FileSystems.getDefault().getPath("").toAbsolutePath().toString();
	}

	public static boolean isNumeric(final String input) {
		if (input == null || input.isEmpty()) {
			return false;
		}
		return IntStream.range(0, input.length())
			.allMatch(i -> Character.isDigit(input.charAt(i)));
	}

	public static int findNextPositivePowerOfTwo(int value) {
		return 1 << (32 - Integer.numberOfLeadingZeros(value - 1));
	}

	public static boolean isEven(final int value) {
		return (value & 0b1) == 0;
	}

	public static boolean isPowerOfTwo(final int value) {
		return value > 0 && ((value & (~value + 1)) == value);
	}

	public static int generateRandomInt() {
		return ThreadLocalRandom.current().nextInt();
	}

	public static String tmpDirName() {
		String tmpDirName = System.getProperty("java.io.tmpdir");
		if (!tmpDirName.endsWith(File.separator)) {
			tmpDirName += File.separator;
		}

		return tmpDirName;
	}

	public static String osName() {
		return System.getProperty("os.name").toLowerCase();
	}

	public static boolean isDebuggerAttached() {
		final RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean();
		return runtimeMXBean.getInputArguments()
			.stream()
			.anyMatch(arg -> arg.contains("-agentlib:jdwp"));

	}

	/**
	 * Input a line of numbers separated by space as integers
	 * and return ArrayList of Integers.
	 * eg. the String "1 2 3 4 5 6 7 8 9" is returned as an ArrayList of Integers.
	 *
	 * @param numbers range of numbers separated by space as a string
	 * @return ArrayList of Integers
	 */

	public static int[] stringToIntegers(String numbers) {
		return Arrays.stream(numbers.split(" ")).mapToInt(Integer::parseInt).toArray();
	}

	/* Class Utilities */

	public static List<Class<?>> getAllInterfaces(final Class<?> cls) {
		return Stream.concat(
			Arrays.stream(cls.getInterfaces()).flatMap(intf ->
				Stream.concat(Stream.of(intf), getAllInterfaces(intf).stream())),
			cls.getSuperclass() == null ? Stream.empty() : getAllInterfaces(cls.getSuperclass()).stream()
		).distinct().collect(Collectors.toList());
	}

	public static boolean isInnerClass(final Class<?> cls) {
		return cls != null && cls.getEnclosingClass() != null;
	}

	public static <E extends Enum<E>> Map<String, E> getEnumMap(final Class<E> enumClass) {
		return Arrays.stream(enumClass.getEnumConstants())
			.collect(Collectors.toMap(Enum::name, Function.identity()));
	}
}
